Skip to content

feat(vortex-array): execute primitive interleave arrays - #9291

Merged
HarukiMoriarty merged 5 commits into
developfrom
nemo/interleave-primitive-execution
Aug 10, 2026
Merged

feat(vortex-array): execute primitive interleave arrays#9291
HarukiMoriarty merged 5 commits into
developfrom
nemo/interleave-primitive-execution

Conversation

@HarukiMoriarty

@HarukiMoriarty HarukiMoriarty commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Summary

  • execute Interleave arrays whose values use primitive dtypes
  • gather from canonical primitive buffers while preserving constant value sources
  • share selector length and bounds validation with the Boolean kernel
  • cover nullable primitive columns, constants, null constants, and invalid selectors

Motivation

Interleave previously only had an execution kernel for Boolean values. This adds the corresponding primitive kernel as a general vortex-array capability. Spatial MakeLine uses this support in the separate stacked PR #9201.

Signed-off-by: Nemo Yu <zyu379@wisc.edu>
@codspeed-hq

codspeed-hq Bot commented Aug 7, 2026

Copy link
Copy Markdown

Merging this PR will regress 1 benchmark

⚡ 3 improved benchmarks
❌ 1 regressed benchmark
✅ 1936 untouched benchmarks
🆕 12 new benchmarks
⏩ 89 skipped benchmarks1

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Simulation cold_misaligned[(64, 256)] 4.4 ms 5.3 ms -17.42%
Simulation decode_varbin[(1000, 4)] 99.3 µs 60.5 µs +64.15%
Simulation slice_primitive_tight_loop[10000] 502.7 µs 438.2 µs +14.74%
Simulation slice_dict_tight_loop[10000] 837.8 µs 738.6 µs +13.44%
🆕 Simulation primitive_mixed_constants[random/n2/nonnull] N/A 420.5 µs N/A
🆕 Simulation primitive_mixed_constants[random/n2/null] N/A 429.1 µs N/A
🆕 Simulation primitive_mixed_constants[random/n64/nonnull] N/A 650.7 µs N/A
🆕 Simulation primitive_mixed_constants[random/n64/null] N/A 714.6 µs N/A
🆕 Simulation primitive_mixed_constants[round_robin/n2/nonnull] N/A 355.9 µs N/A
🆕 Simulation primitive_mixed_constants[round_robin/n2/null] N/A 357.2 µs N/A
🆕 Simulation primitive[random/n2/nonnull] N/A 538.1 µs N/A
🆕 Simulation primitive[random/n2/null] N/A 545.2 µs N/A
🆕 Simulation primitive[random/n64/nonnull] N/A 869.9 µs N/A
🆕 Simulation primitive[random/n64/null] N/A 908.7 µs N/A
🆕 Simulation primitive[round_robin/n2/nonnull] N/A 376.6 µs N/A
🆕 Simulation primitive[round_robin/n2/null] N/A 383.9 µs N/A

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing nemo/interleave-primitive-execution (a0ad6c9) with develop (e77f520)2

Open in CodSpeed

Footnotes

  1. 89 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

  2. No successful run was found on develop (b4b8f54) during the generation of this report, so e77f520 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

Signed-off-by: Nemo Yu <zyu379@wisc.edu>
Comment on lines +122 to +125
let len = validate_selectors(values.len(), |branch| values[branch].len(), branches, rows)?;
let mut output = BufferMut::with_capacity(len);
for i in 0..len {
output.push(values[branches[i].as_()].value(rows[i].as_()));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do you validate once and then used checked accesses?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You likely want to zip with idx with the output and avoid the bounds check on the output

Comment on lines 31 to 39
if array.value(0).dtype().is_boolean() {
bool::execute(array, ctx)
} else if array.value(0).dtype().is_primitive() {
primitive::execute(array, ctx)
} else {
let value_dtype = array.value(0).dtype().clone();
vortex_panic!(
"interleave execution is only implemented for boolean values; value dtype {} is not \
yet supported",
"interleave execution is not implemented for value dtype {}",
value_dtype

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know this is my bad code but the return dtype can we used not the value(0) type

Comment on lines +35 to +37
if array.value(i).as_opt::<Constant>().is_none() {
array = require_child!(array, array.value(i), i + 2 => Primitive);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can use Columnar here

_ctx: &mut ExecutionCtx,
) -> VortexResult<ExecutionResult> {
let num_values = array.num_values();
array = require_child!(array, array.array_indices(), 0 => Primitive);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this could be bool if thre are two values. I cannot remember if we kept that?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically yes for exactly two values. The current Interleave implementation explicitly rejects Boolean array_indices, including the test from the original encoding PR, so I assumed that restriction was intentional. I’ve left it unchanged, but can add the two-value Boolean special case if that is now desired.

//! ## Selector types
//!
//! `array_indices` encodes the value array per row as a non-nullable **unsigned integer**
//! (`array_indices[i]` is the index into `values`). `row_indices` is likewise a non-nullable
//! **unsigned integer** naming the position within the selected value array.

// `row_indices` names a position within the selected value.
for (name, selector) in [
("array_indices", array_indices),
("row_indices", row_indices),
] {
match selector.dtype() {
DType::Primitive(ptype, nullability) if ptype.is_unsigned_int() => {
vortex_ensure!(
!nullability.is_nullable(),
"interleave {name} must be non-nullable, got {}",
selector.dtype()
);
}
other => vortex_bail!(
"interleave {name} must be a non-nullable unsigned integer, got {other}"
),
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think we should do here

Comment on lines +50 to +53
enum PrimitiveValues<T> {
Buffer(Buffer<T>),
Constant { value: T, len: usize },
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we have this code everywhere? Also you don't need the len

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the len. Existing types are either type-erased or operation-specific; I think this keeps the hot loop typed and local minimum.

let row = (*row).as_();
// SAFETY: the caller guarantees that the selected branch and row are in bounds for
// `values` and the selected physical value buffer.
slot.write(unsafe { values.get_unchecked(branch).value_unchecked(row) });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you want to do this unchecked vs just using the checked one?

Comment on lines +64 to +65
Self::Buffer(values) => *unsafe { values.get_unchecked(index) },
Self::Constant(value) => *value,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we could convert this from a branch into a offset load for the constant?

values[index & ty] where ty = usize::MAX if buffer and zero otherwise.

I am not sure what is faster

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one is actually faster.

@HarukiMoriarty
HarukiMoriarty merged commit 9157d6c into develop Aug 10, 2026
82 of 83 checks passed
@HarukiMoriarty
HarukiMoriarty deleted the nemo/interleave-primitive-execution branch August 10, 2026 16:54
HarukiMoriarty added a commit that referenced this pull request Aug 10, 2026
## Rationale

Vortex native geometry arrays should support constructing two-point
LineStrings directly from paired Point columns without an Arrow or WKB
round trip.

## What changes are included?

- Add vortex.st.make_line for paired native Point arrays.
- Promote mixed XY, XYZ, XYM, and XYZM inputs, filling absent ordinates
with zero.
- Propagate CRS metadata, constants, and endpoint nulls.
- Build native LineString storage directly and add focused tests and
CodSpeed benchmarks.

## Stack

#9291 (primitive Interleave execution) has merged into develop, so this
PR is now standalone: rebased onto develop, it contains only the spatial
function.

ST_Length was split into #9290 so each scalar function can also be
reviewed independently.

Signed-off-by: Nemo Yu <zyu379@wisc.edu>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

changelog/feature A new feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants